﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.IO.Packaging;
using System.Linq;
using System.Reflection;
using System.Resources;
using System.Windows;
using System.Windows.Baml2006;
using System.Windows.Data;
using System.Xaml;
using DependencyAnalysis.DataModel;

namespace DependencyAnalysis.Internal
{
    public class XamlAnalyzer
    {
        private Application _dummyApplication;
        private static readonly Type StaticResourceExtensionType = typeof(StaticResourceExtension);
        private static readonly Type DynamicResourceExtensionType = typeof(DynamicResourceExtension);
        private static readonly Type ResourceDictionaryType = typeof(ResourceDictionary);
        private static readonly Type FrameworkElementType = typeof(FrameworkElement);
        private static readonly Type BindingType = typeof(Binding);
        private static readonly MemberInfo NameMember;
        private static readonly MemberInfo SourceMember;
        private static readonly MemberInfo ElementNameMember;
        private static readonly HashSet<Type> IgnoredTypes = new HashSet<Type> { DynamicResourceExtensionType, StaticResourceExtensionType, BindingType, typeof(Setter) };

        static XamlAnalyzer()
        {
            NameMember = FrameworkElementType.GetMember("Name")[0];
            SourceMember = ResourceDictionaryType.GetMember("Source")[0];
            ElementNameMember = BindingType.GetMember("ElementName")[0];
        }

        private class XamlEntry
        {
            public Assembly Assembly { get; set; }
            public ModuleDescriptor Module { get; set; }
            public Stream Content { get; set; }
            public string FileName { get; set; }
            public bool IsAnalyzed { get; set; }
            public MemberDescriptor Member { get; set; }
        }

        private readonly Dictionary<string, XamlEntry> _xamlFiles = new Dictionary<string, XamlEntry>();

        private readonly Dictionary<TypeDescriptor, List<TypeDescriptor>> _xamlHierarchy = new Dictionary<TypeDescriptor, List<TypeDescriptor>>();

        private readonly Dictionary<TypeDescriptor, Dictionary<string, MemberDescriptor>> _resources = new Dictionary<TypeDescriptor, Dictionary<string, MemberDescriptor>>();

        public IDependencyModel Model { get; set; }

        public void Analyze(IEnumerable<Assembly> modules)
        {
            if (!UriParser.IsKnownScheme("pack"))
            {
                _dummyApplication = new Application();
            }
            foreach (var module in modules)
            {
                var moduleDescriptor = DescriptorBuilder.BuildModuleDescriptor(module);
                Stream stream = module.GetManifestResourceStream(module.GetName().Name + ".g.resources");
                if (stream == null) continue;
                using (var reader = new ResourceReader(stream))
                {
                    foreach (DictionaryEntry dictionaryEntry in reader)
                    {
                        var xamlEntry = new XamlEntry
                        {
                            Module = moduleDescriptor,
                            Assembly = module,
                            Content = (Stream)dictionaryEntry.Value,
                            FileName = (string)dictionaryEntry.Key,
                        };
                        string fileName = xamlEntry.FileName;
                        var lastIndexOf = fileName.LastIndexOf('/');
                        string path = moduleDescriptor.Name;
                        if (lastIndexOf > -1)
                        {
                            path += "." + fileName.Substring(0, lastIndexOf);
                        }
                        var submodule = DescriptorBuilder.BuildSubmoduleDescriptor(moduleDescriptor, path);
                        if (!moduleDescriptor.Submodules.Contains(submodule))
                        {
                            moduleDescriptor.Submodules.Add(submodule);
                        }
                        Model.Add(submodule);
                        var typeDescriptor = DescriptorBuilder.BuildXamlTypeDescriptor(moduleDescriptor, fileName);
                        if (!submodule.Types.Contains(typeDescriptor))
                        {
                            submodule.Types.Add(typeDescriptor);
                        }
                        Model.Add(typeDescriptor);
                        var memberDescriptor = DescriptorBuilder.BuildXamlMemberDescriptor(typeDescriptor, string.Empty);
                        if (!typeDescriptor.Members.Contains(memberDescriptor))
                        {
                            typeDescriptor.Members.Add(memberDescriptor);
                        }
                        Model.Add(memberDescriptor);
                        xamlEntry.Member = memberDescriptor;
                        _xamlFiles.Add(memberDescriptor.ComparableIdentifier, xamlEntry);
                    }
                }
            }
            foreach (var xaml in _xamlFiles)
            {
                AnalyzeXaml(xaml.Value);
            }
        }

        private void AnalyzeXaml(XamlEntry entry)
        {
            if (entry.IsAnalyzed)
            {
                return;
            }
            string fileName = entry.FileName;
            var localAssembly = entry.Assembly;
            var content = entry.Content;
            var memberDescriptor = entry.Member;
            _resources.Add(memberDescriptor.ImplementingType, new Dictionary<string, MemberDescriptor>());
            entry.IsAnalyzed = true;
            if (fileName.EndsWith(".baml"))
            {
                var reader = new Baml2006Reader(content, new XamlReaderSettings { ValuesMustBeString = true, LocalAssembly = localAssembly });
                Traverse(memberDescriptor, reader);
            }
            else if (fileName.EndsWith(".xaml"))
            {
                var reader = new XamlXmlReader(content, new XamlXmlReaderSettings { ValuesMustBeString = true, LocalAssembly = localAssembly });
                Traverse(memberDescriptor, reader);
            }
        }

        /// <summary>
        /// read the XAML file in a SAX parser way
        /// </summary>
        /// <param name="xamlMemberDescriptor"></param>
        /// <param name="reader"></param>
        private void Traverse(MemberDescriptor xamlMemberDescriptor, XamlReader reader)
        {
            _xamlHierarchy[xamlMemberDescriptor.ImplementingType] = new List<TypeDescriptor>();
            var genericMemberNames = new Dictionary<string, int>();
            AddImplicitDictionaries(xamlMemberDescriptor);

            bool storeName = false;
            bool storeKey = false;
            bool resolveElementReference = false;
            bool resolveResourceReference = false;
            bool resolveDictionaryReference = false;

            MemberDescriptor lastMember = null;
            try
            {
                while (reader.Read())
                {
                    if (reader.NodeType == XamlNodeType.StartObject)
                    {
                        var member = AddObjectReference(xamlMemberDescriptor, reader, genericMemberNames);
                        if (reader.Type.UnderlyingType == StaticResourceExtensionType ||
                            reader.Type.UnderlyingType == DynamicResourceExtensionType)
                        {
                            resolveResourceReference = true;
                        }
                        else
                        {
                            if (!BindingType.IsAssignableFrom(reader.Type.UnderlyingType))
                            {
                                lastMember = member;
                            }
                        }
                    }
                    else if (reader.NodeType == XamlNodeType.StartMember && reader.Member != null)
                    {
                        if (reader.Member.UnderlyingMember != null)
                        {
                            var dependency =
                                DescriptorBuilder.BuildDependencyDescriptorForMember(reader.Member.UnderlyingMember,
                                    xamlMemberDescriptor, ReferenceType.AccessMember);
                            Model.Add(dependency);
                            if (reader.Member.UnderlyingMember == SourceMember)
                            {
                                resolveDictionaryReference = true;
                            }
                            else if (reader.Member.UnderlyingMember == ElementNameMember)
                            {
                                resolveElementReference = true;
                            }
                        }
                        storeKey = reader.Member == XamlLanguage.Key;
                        storeName = reader.Member == XamlLanguage.Name ||
                                    reader.Member.UnderlyingMember != null &&
                                    reader.Member.UnderlyingMember == NameMember;
                    }
                    else if (reader.NodeType == XamlNodeType.Value)
                    {
                        if (storeName || storeKey)
                        {
                            var memberDescriptor =
                                DescriptorBuilder.BuildXamlMemberDescriptor(xamlMemberDescriptor.ImplementingType,
                                    reader.Value + "");
                            xamlMemberDescriptor.ImplementingType.Members.Add(memberDescriptor);
                            Model.Add(memberDescriptor);
                            var resources = _resources[xamlMemberDescriptor.ImplementingType];
                            if (storeKey && !resources.ContainsKey(reader.Value + ""))
                            {
                                resources.Add(reader.Value + "", memberDescriptor);
                            }
                        }
                        if (lastMember != null && (resolveResourceReference || resolveElementReference))
                        {
                            var success = ResolveElementReference(xamlMemberDescriptor, reader, lastMember);
                            if (!success && resolveResourceReference)
                            {
                                ResolveResourceReference(xamlMemberDescriptor, reader, lastMember);
                            }
                        }
                        if (resolveDictionaryReference)
                        {
                            ResolveDictionaryReference(xamlMemberDescriptor, reader);
                        }
                        storeName = false;
                        storeKey = false;
                        resolveElementReference = false;
                        resolveResourceReference = false;
                        resolveDictionaryReference = false;
                    }
                }
            }
            catch (Exception ex)
            {
                if (!(ex is FileNotFoundException || ex is NotImplementedException))
                {
                    throw;
                }
            }
        }

        private void AddImplicitDictionaries(MemberDescriptor xamlMemberDescriptor)
        {
            var appXaml = DescriptorBuilder.BuildXamlTypeDescriptor(xamlMemberDescriptor.ImplementingType.Submodule.Module,
                "app.xaml");
            if (_xamlFiles.ContainsKey(appXaml.ComparableIdentifier))
            {
                _xamlHierarchy[xamlMemberDescriptor.ImplementingType].Add(appXaml);
                if (!_resources.ContainsKey(appXaml))
                {
                    var xamlEntry = _xamlFiles[appXaml.ComparableIdentifier];
                    if (!xamlEntry.IsAnalyzed)
                    {
                        AnalyzeXaml(xamlEntry);
                    }
                }
            }
        }

        private void ResolveResourceReference(MemberDescriptor xamlMemberDescriptor, XamlReader reader,
            MemberDescriptor lastMember)
        {
            var visitedDictionaries = new HashSet<TypeDescriptor>();
            var nextLevel = new List<TypeDescriptor>();
            nextLevel.AddRange(_xamlHierarchy[xamlMemberDescriptor.ImplementingType]);
            bool found = false;
            while (!found && nextLevel.Count > 0)
            {
                var currentLevel = nextLevel.ToArray();
                nextLevel.Clear();
                foreach (var dictionary in currentLevel)
                {
                    if (visitedDictionaries.Contains(dictionary) || !_resources.ContainsKey(dictionary))
                    {
                        continue;
                    }
                    visitedDictionaries.Add(dictionary);
                    var resources = _resources[dictionary];
                    var key = "" + reader.Value;
                    if (resources.ContainsKey(key))
                    {
                        var resourceMember = resources[key];
                        var dependency = DescriptorBuilder.BuildDependencyDescriptor(lastMember,
                            resourceMember, ReferenceType.AccessMember);
                        Model.Add(dependency);
                        found = true;
                        break;
                    }
                    nextLevel.AddRange(_xamlHierarchy[dictionary]);
                }
            }
        }

        private bool ResolveElementReference(MemberDescriptor xamlMemberDescriptor, XamlReader reader,
            MemberDescriptor lastMember)
        {
            var resourceMember = xamlMemberDescriptor.ImplementingType.Members.FirstOrDefault(
                x => x.Name == "" + reader.Value);
            if (resourceMember != null && lastMember != null)
            {
                var dependency = DescriptorBuilder.BuildDependencyDescriptor(lastMember, resourceMember,
                    ReferenceType.AccessMember);
                Model.Add(dependency);
                return true;
            }
            return false;
        }

        private void ResolveDictionaryReference(MemberDescriptor xamlMemberDescriptor, XamlReader reader)
        {
            bool resolveFirst = false;
            var dictionaryLocator = "" + reader.Value;
            dictionaryLocator = dictionaryLocator.Replace("\\", "/");
            MemberDescriptor targetXamlMember = null;
            try
            {
                if (dictionaryLocator.StartsWith("pack:"))
                {
                    var uri = new Uri(dictionaryLocator, UriKind.RelativeOrAbsolute);
                    dictionaryLocator = PackUriHelper.GetPartUri(uri).ToString();
                }
                var parts = dictionaryLocator.TrimStart('/').Split(new[] { ";component/" }, StringSplitOptions.None);
                var targetModule = Model.DependenciesPerModule.Keys.FirstOrDefault(x => ((ModuleDescriptor)x).Name == parts[0]) ?? xamlMemberDescriptor.ImplementingType.Submodule.Module;

                var targetXamlType = DescriptorBuilder.BuildXamlTypeDescriptor((ModuleDescriptor)targetModule,
                    (parts.Length == 1 ? parts[0] : parts[1]).TrimStart('/', '.').ToLower());
                if (_xamlFiles.ContainsKey(targetXamlType.ComparableIdentifier))
                {
                    resolveFirst = true;
                }

                targetXamlMember = DescriptorBuilder.BuildXamlMemberDescriptor(targetXamlType, string.Empty);
                _xamlHierarchy[xamlMemberDescriptor.ImplementingType].Add(targetXamlType);
                var dependency = DescriptorBuilder.BuildDependencyDescriptor(xamlMemberDescriptor, targetXamlMember,
                    ReferenceType.AccessType);
                Model.Add(dependency);
            }
            catch (UriFormatException)
            {
                //// intentionally left blank
            }
            if (resolveFirst && targetXamlMember != null && _xamlFiles.ContainsKey(targetXamlMember.ComparableIdentifier))
            {
                var xamlEntry = _xamlFiles[targetXamlMember.ComparableIdentifier];
                if (!xamlEntry.IsAnalyzed)
                {
                    AnalyzeXaml(xamlEntry);
                }
            }
        }

        private MemberDescriptor AddObjectReference(MemberDescriptor xamlMemberDescriptor, XamlReader currentReader,
            Dictionary<string, int> genericMemberNames)
        {
            if (currentReader.Type.UnderlyingType != null && !IgnoredTypes.Contains(currentReader.Type.UnderlyingType))
            {
                var underlyingType = currentReader.Type.UnderlyingType;
                var name = underlyingType.Name;
                var genericNameIndex = genericMemberNames.ContainsKey(name) ? genericMemberNames[name] : 0;
                genericNameIndex++;
                genericMemberNames[name] = genericNameIndex;
                var currentMember = new MemberDescriptor
                {
                    Name = name + "'" + genericNameIndex,
                    ImplementingType = xamlMemberDescriptor.ImplementingType,
                    Identifier = xamlMemberDescriptor.Identifier + ".$" + name + "'" + genericNameIndex,
                    FullName = xamlMemberDescriptor.FullName + ".$" + name + "'" + genericNameIndex,
                    MemberType = MemberType.Field,
                };
                xamlMemberDescriptor.ImplementingType.Members.Add(currentMember);
                Model.Add(currentMember);
                var dependency =
                    DescriptorBuilder.BuildDependencyDescriptorForTypeReference(underlyingType,
                        currentMember, ReferenceType.Field);
                Model.Add(dependency);
                return currentMember;
            }
            return null;
        }
    }
}
